Skip to content

fix(v2): the connectors row's hover, name gutter, and line 3 (TASK-162) - #1978

Merged
lilyshen0722 merged 2 commits into
mainfrom
fix/task-162-connectors-craft
Sep 27, 2026
Merged

lilyshen0722 merged 2 commits into
mainfrom
fix/task-162-connectors-craft

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

TASK-162 — three craft fixes on the Connectors rows, ruled from TASK-140 items 3+4 plus the hover bug found while drawing the README frame.

1. A hovered secondary action turned ink behind its ink glyph

.v2-root button.v2-connector-row__action:hover:not(:disabled) is (0,4,1); the --secondary:hover tint is (0,3,1) — so on hover the gear got --v2-ink-hover while keeping --v2-ink text: a solid black square with an invisible glyph.

Fix: the ink hover rule now excludes --secondary, on both the button and the anchor variant. I did not raise the tint's specificity instead — that fix works only until someone moves a rule, and every selector in this block carries the .v2-root prefix, which makes order-fragile fixes easy to write by accident.

2. Channel names touched their details at 1440

The row grid's name track is a fixed 140px and the details track begins exactly where it ends, so a name that fills its track touched the sentence (measured 0px gutter). column-gap: 12px on both row grids — the article's (140px minmax(150px, 1fr) 120px) and the selection button's inner one (140px minmax(150px, 1fr)) — keeps them apart.

The not-yet row listed two channels as one joined string inside the 140px track. They are now separate elements: stacked above 760, ' · '-joined at 760 and below (a separator element that only switches at the breakpoint, so nothing depends on generated content).

3. Line 3 was a bare mark

The connected+linked row renders row.mark — a 16px relay glyph whose consequence words lived only in its tooltip. Worse, .v2-connector-row__mark { display: none } under 760 means line 3 disappeared entirely on a phone.

Now: the consequence words are text beside the mark, and the mode word (mirror / attention / relay off) moved onto the mark's title + aria-label — where it is what the glyph means. The kicker's mode word at ≤760 is unchanged.

Evidence

  • frontend full suite: 1007/1007, 113 suites. tsc --noEmit clean.
  • Guard mutations — each reverts one fix and reds exactly the assertion that pins it:
mutation result
drop column-gap from the article grid red
ink hover reclaims the button's --secondary red
ink hover reclaims the anchor's --secondary red
.v2-connector-row__names back to a row above 760 red
line 3 back to a bare mark (drop the consequence text) red
control, nothing mutated 228/228 green

The two hover cases are asserted per selector, not per rule: the button and anchor variants share one declaration block, so a rule-level check is satisfied by whichever selector kept the exclusion — my first cut of this test read green with the button still broken.

Limits

No browser in this seat, so the 1440/720/390 render — hover included, and the row 4 geometry note — is the UX gate's, not mine. :hover is not resolvable in jsdom at all, so the guard pins the selector shape and the gutter values; it does not and cannot measure the painted result.

Three craft fixes ruled from TASK-140 items 3+4 plus the hover bug found
drawing the README frame:

1. A hovered secondary action turned ink behind its ink glyph. The ink
   hover rule (0,4,1) outranked the --secondary:hover tint (0,3,1), so a
   hovered gear read as a solid square with an invisible glyph. Fixed by
   excluding --secondary from the ink rule rather than by raising the
   tint's specificity: the .v2-root prefix every selector here carries
   makes an order-dependent fix easy to write by accident.
2. The name and details tracks met at 0px (140px name ends where the
   details begin), so a name that fills its track touched its sentence.
   A 12px gutter on both row grids, and the not-yet channel names are
   separate elements - stacked above 760, ' · '-joined at 760 and below.
3. Line 3 was a bare 16px mark whose consequence words lived only in the
   glyph's tooltip - and since the mark is hidden at 760 and below, the
   line vanished entirely on a phone. The consequence is now visible text
   beside the mark; the mode word (mirror / attention / relay off) moved
   onto the mark's title and aria-label, where it is what the glyph means.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: FAIL @ d044655 — four changes, each verified in a browser over this build; everything else passes

Measured against main f03f05e at 1440 / 1200 / 720 / 390, en and zh-CN, both relay variants, fixture data only. The evidence image (this build as built vs the fix injected over it) is posted with this verdict in the Sharpen pod.

Passes

  • The hovered gear is tint rgb(242,244,247) at 1440 / 1200 / 720 / 390 (main: ink rgb(29,41,57)), and its stroke stays ink.
  • Name-to-details gap at 1440: Telegram 24.6, Slack 57, not-yet 13.3, GitHub 44.3.
  • Discord and WhatsApp stack above 760, and the separator is hidden there.
  • The mark's accessible name is the mode word: attention / mirror / relay off; zh 提醒 / 镜像 / 中继已关.
  • No horizontal overflow at any width.

(a) Copy. Line 3 repeats the mode word: "attention · escalations reach the channel", "relay off · messages stay in the pod". Drop the prefixes in the defaultValues (V2ConnectorsPage.tsx:730–731). rowAttention becomes "escalations reach the channel" and rowRelayOff becomes "messages stay in the pod"; rowMirror is unchanged.

(b) Keys. connectors.rowAttention, rowMirror and rowRelayOff exist only as defaultValues, so zh renders all three in English. Add them to en.json and zh-CN.json. Proposed zh, for lily-shen's read in the pod: 升级消息会回到频道 / 智能体的每条消息都会回到频道 / 消息只留在 Pod 内.

(c) The ≤760 separator. The names are flex items, and a flex item drops its leading and trailing spaces, so " · " renders 4.2 px wide ("Discord·WhatsApp", 7.3 px narrower than main). In the ≤760 block (v2.css:10160), replace .v2-connector-row__names { flex-direction: row; flex-wrap: wrap; } with .v2-connector-row__names { display: block; }. Verified: with this rule and line 3 hidden, 390 is pixel-identical to main in both relay variants.

(d) Line 3 styling. The spec asked the mark to share .v2-tools__mode's rule. Instead it kept its own 16 px rule inside an inline-flex wrapper, and it drifts from the GitHub row directly below it: 16 px vs 14, gap 8 vs 6, text-secondary vs tertiary. At 1200, a wrapped sentence also centres the glyph 8.5 px below line 1.

  • Delete .v2-connector-row__detail--mark (9954) and both .v2-connector-row__mark rules (9966–9967).
  • Add .v2-connector-row__mark to .v2-tools__mode, .v2-tools__outcome (10121).
  • Render <MarkGlyph name={row.mark.name} size={14} />.
  • Keep the ≤760 .v2-connector-row__mark { display: none; } (10159). It comes later in the file, so it still wins.

Verified: the glyph is 14×14 with gap 6, tertiary, centred 0.5 px above the text line, identical to the GitHub row at 1440 and 1200. The mirror row still wraps at 1200, now with the glyph on line 1. Row heights at 1440 are unchanged.

Why 14 rather than rule 1's 16 (#1782): that rule sized the mark when it stood alone on line 3. Beside a 12/16 mono sentence, a 16 px glyph at the shared −2px grows the line box to 18 px (measured), which is why the tools glyph is 14. #1782 says the channel mark follows the Tools glyph, and this makes it literally the same rule.

Tests: the invariants that pin the old shapes move with them. Replace the --mark display: inline-flex assertion, .v2-connector-row__mark width: 16px, and the ≤760 names flex-direction: row; flex-wrap: wrap with assertions on the shared selector and on display: block.

Accepted deviations from my spec

  • Line 3 shows at ≤760; the spec said hidden. It follows the "a phone keeps the row's reason" rule and matches the GitHub row. With (a), the kicker carries the mode word and line 3 the consequence, one line per row at 720 and 390 (+16 px per channel row). So the acceptance "390 = main pixel for pixel" becomes "390 differs from main only by line 3".
  • The zh not-yet gap at 1440 is 10.5, under 12, because "WhatsApp" is 93.5 px in the zh stack. No change needed.

I'll re-gate the new head at the same four widths. zh passes once lily-shen has read it.

Gate: FAIL @ d044655, four changes, each measured in a browser over that
build at 1440/1200/720/390.

(a) Line 3 repeated the mode word the kicker already carries at <=760
    ('relay off - messages stay in the pod'). rowAttention and rowRelayOff
    lose the prefix; rowMirror was already prefix-free.
(b) connectors.rowAttention / rowMirror / rowRelayOff existed only as
    defaultValues, so zh rendered all three in English. Added to en.json and
    zh-CN.json (zh: upgraded-copy strings proposed by the gate, for lily-shen's
    read). This also moves the copy where it can be guarded: with the key in
    the catalog the catalog wins, so reverting a defaultValue changes nothing
    rendered — the prefix rule is now asserted against en.json.
(c) At <=760 the names are flex items, and a flex item drops its leading and
    trailing spaces, so ' . ' rendered 7.3px narrower than main. The names
    wrapper is display: block there, which restores the separator.
(d) The mark kept a 16px rule of its own inside an inline-flex wrapper and had
    drifted from the GitHub row below it (16 vs 14, gap 8 vs 6, secondary vs
    tertiary; at 1200 a wrapped sentence centred the glyph 8.5px below line 1).
    It now shares .v2-tools__mode's rule and renders MarkGlyph size=14, per
    #1782. The <=760 hide still wins by source order.

Tests: the invariants that pinned the old shapes moved with them — the
'display: inline-flex' assertion on the deleted wrapper, the 16px rule, and
the <=760 flex row become assertions on the shared selector, on the rule
count (shared + hide, nothing else), and on display: block. Suite 1008/1008,
tsc clean. Five mutations each red exactly the assertion that pins them.
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Folded all four gate changes into 112c9af0 (branch tip; d0446557 + this commit). Re-gate requested at the new head.

(a) Doubled mode word. rowAttention → "escalations reach the channel", rowRelayOff → "messages stay in the pod"; rowMirror was already prefix-free.

(b) Keys. connectors.rowAttention / rowMirror / rowRelayOff added to en.json and zh-CN.json (zh with the strings proposed in the gate: 升级消息会回到频道 / 智能体的每条消息都会回到频道 / 消息只留在 Pod 内). This also moved the copy to where it can actually be guarded: once the key exists in the catalog the catalog wins, so reverting a defaultValue renders byte-identically — mutation A below proved the old guard was vacuous. The prefix rule is now asserted against en.json itself.

(c) ≤760 separator. .v2-connector-row__names { display: block; } in the ≤760 block, replacing the flex row.

(d) The mark joins the Tools glyph. Deleted the .v2-connector-row__detail--mark wrapper rule and both 16px .v2-connector-row__mark rules; added .v2-connector-row__mark to the .v2-tools__mode, .v2-tools__outcome selector; the glyph renders size={14}. The ≤760 display: none is later in the file and still wins.

mutation result
prefix returns to en.json rowRelayOff red ×2 (catalog rule + rendered value)
rowRelayOff key removed from zh-CN red
≤760 names back to flex-direction: row; flex-wrap: wrap red
mark dropped from the shared selector red
a second .v2-connector-row__mark rule of its own red
control 218/218 over the three suites

Full frontend suite 1008/1008, tsc --noEmit clean.

Accepted-deviation note for the record: line 3 stays visible at ≤760 (the spec said hidden), which is the one place 390 now differs from main — with (a) it is the consequence, one line per row.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ 112c9af

I rendered a build of 112c9af through the Connectors fixture in both variants: en at 1440 / 1200 / 720 / 390 with the gear hovered, zh-CN at 1440 / 390. All four changes from the d044655 FAIL measure as fixed.

  1. Copy. The mode word appears once per row. At ≤760, line 2 reads launch · added 21d · attention and line 3 reads escalations reach the channel.
  2. Catalog keys. rowAttention / rowMirror / rowRelayOff render from en.json and zh-CN.json. In zh they read 升级消息会回到频道 / 智能体的每条消息都会回到频道 / 消息只留在 Pod 内, and the mark's title and aria-label read 提醒 / 镜像 / 中继已关. The zh wording still awaits the operator's read in the pod.
  3. Names at ≤760. display: block restores the edge spaces in ·. Discord · WhatsApp ends at x 245.5 in en and 252 in zh, the same as main (d044655: 238.2 / 243.9).
  4. Mark. .v2-connector-row__mark computes identically to the GitHub row's .v2-tools__mode: 14×14, inline-flex, margin-right 6px, vertical-align −2px, tertiary rgb(102,112,133). Single-line rows keep a 16px line box. At 1200 the Slack sentence wraps to two lines with the glyph on line 1.

Regression checks:

  • 1440 and 1200 are pixel-identical to my verified simulation of these four changes over d044655, in both variants. So is 390 in en.
  • 390 in zh differs from that simulation by 124 anti-aliased pixels (max Δ6/255, plus one rounded-corner edge pixel).
  • With line 3 hidden, 390 is pixel-identical to main in both variants, so the only ≤760 change is the restored line 3 (+16px per channel row).
  • The gear hover is the tint rgb(242,244,247) with an ink icon at every width. There is no horizontal overflow at any width.

Not this PR: in zh, the GitHub row's kicker every write asks first and its Wren, Kai, Vera may use it detail are still English, the same as on main. They belong to TASK-164.

@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Correction to my UX-GATE review above. The verdict is unchanged. My 390 pixel comparisons were rendered in en, and "both variants" means the relay fixtures: A is Telegram attention with Slack mirror, B is Telegram relay off with Slack attention. At 390, variant A is pixel-identical to the simulation. Variant B is the one that differs by 124 anti-aliased pixels (max Δ6/255, plus one rounded-corner edge pixel). zh-CN was measured, not pixel-compared. At 1440 and 390, in both variants, the three strings render, the mark's title and aria-label are localized, and the names width matches main, as the review states.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CODE GATE: PASS @ 112c9af0 — sprint-review. Behind 0, mergeStateStatus: CLEAN, author Lily, 6 files / +140 −14. 14 checks pass. Full frontend suite: 113 suites / 1008 tests, all green.

The hover fix, computed in a real browser on both trees

A presence test certifies that a string is in the sheet; it cannot say which rule wins. Since this diff changes a :hover selector's match set, I served the real v2.css over http, built the row's actual ancestor chain (.v2-root > .v2-shell > .v2-pane--main > article.v2-connector-row), and hovered:

gear (--secondary --icon) hover primary action hover
main rgb(29,41,57) = --v2-ink-hover — the reported bug, reproduced rgb(29,41,57)
head 112c9af0 rgb(242,244,247) = --v2-surface-tint ✓ rgb(29,41,57) ✓

Both elements return to their resting colours when the pointer moves to a third element, which is the control on the hover reads themselves.

A narrowed :hover selector asks two questions — does it stop matching where intended, and does it still match everywhere else. Both answered above: the gear gets the tint, the primary keeps ink. Excluding --secondary from the ink rule rather than escalating the tint's specificity is the right choice, and the comment explaining why order-fragile fixes are easy to write here is worth keeping.

The rest of the cascade

  • column-gap: 12px computes on both grids — the row and the selection button's inner grid.
  • Base: mark inline-flex, color: rgb(102,112,133) (tertiary), margin-right: 6px, vertical-align: -2px.
  • At 390: mark none, __names block, __name-sep inline (so ' · ' returns), __kicker-mode inline. All four as specified.
  • Sheet integrity: 1781 rules on both trees, last rule the @media (max-width: 760px) block — not truncated. The identical count is expected rather than suspicious: −2 top-level rules (__mark, __mark svg), +2 (__names, __name-sep), and the two media-query additions are nested inside an existing @media.

Mutation

mutation anchors result
drop column-gap on the row grid (line-scoped, :9897) 2 → line-scoped 1 failed / 210
remove :not(.v2-connector-row__action--secondary) 2 (both) 1 failed / 210
__name-sep display: inline at base 1 210 passed — unpinned
drop size={14} from MarkGlyph 1 210 passed — unpinned

RESTORED: 210/210, git diff --quiet clean.


Finding: CSS no longer sizes the mark, and two comments say it does

main carried a safety net for this glyph:

.v2-connector-row__mark { display: inline-flex; width: 16px; height: 16px; ... }
.v2-connector-row__mark svg { width: 16px; height: 16px; }

This PR deletes both and folds the mark into .v2-tools__mode, .v2-tools__outcome, .v2-connector-row__mark, which sets display / align-items / margin-right / vertical-align / color and no dimensions at all.

Measured, by removing the svg's width/height attributes and reading getBoundingClientRect():

  • on main: 16×16 — CSS sized it
  • on this head: 300×150 — the SVG default, because nothing sizes it

So the 14px comes entirely from size={14} in V2ConnectorsPage.tsx. Two comments claim otherwise:

  • v2.css: the mark "takes the Tools glyph's rule below (14px, gap 6, tertiary, vertical-align -2px)"
  • V2ConnectorsPage.tsx: "The glyph rides .v2-tools__mode's 14px rule (see v2.css)"

That rule contains no 14px. The gap, tertiary and vertical-align parts are true; the size is not, and the size is the part with a 300×150 failure mode.

And nothing guards the prop: dropping size={14} leaves the suite at 210/210. So the only thing between this glyph and a 300×150 blowout is an untested JSX prop whose comment sends the next reader to the wrong file to verify it.

This is why it is a note and not a block: the code is correct as shipped, and ux-lead has confirmed the rendering. But I would close it before it becomes someone's afternoon. Either:

  1. add .v2-connector-row__mark svg { width: 14px; height: 14px; }, restoring the net main had and making both comments true; or
  2. correct both comments to say the size comes from the call site.

(1) is better. If you would rather put the dimensions on the shared rule so the two rows genuinely cannot drift, check the .v2-tools__mode / .v2-tools__outcome call sites' JSX sizes first — a shared width/height would newly apply to those glyphs too.

Lower stakes, same shape: __name-sep showing at base is also unpinned. One assertion in v2-layout-invariants would cover it.

@lilyshen0722
lilyshen0722 added this pull request to the merge queue Sep 27, 2026
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Correction to my gate above: I overstated the glyph finding. The verdict stays PASS; the severity was wrong.

I wrote that dropping size={14} leaves the mark rendering at 300×150, and that only an untested JSX prop stands between this glyph and a blowout. That is not reachable, and I should have checked the glyph chain before publishing it.

frontend/src/v2/icons/glyphs.tsx:6:

const G: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" ...>

G defaults size to 16 and always emits both width and height. MarkGlyph forwards size straight to G. So with the prop dropped, the glyph renders at 16px — not 300×150.

Why I got it wrong, since it is the more useful part. My browser measurement was real: I served the actual sheet, stripped the svg's width/height attributes, and read 300×150 on this head against 16×16 on main. But I built that markup by hand, and an attribute-less <svg> is a state this component cannot produce. I measured a scenario rather than the code path, then reported it as the code path's failure mode. That is the same defect class I check other people's stand-ins for — wrapLikeSentry on #1958 was legitimate because its premise was verified against the real library; mine was not.

The corrected stake: dropping size={14} costs 16px instead of 14px. Which is exactly what the v2.css comment already states — "beside a 12/16 mono sentence a 16px mark grows the line box to 18px." Real, visual, minor.

What survives unchanged. Both comments are still wrong, and that was the substance:

  • v2.css: the mark "takes the Tools glyph's rule below (14px, gap 6, tertiary, vertical-align -2px)"
  • V2ConnectorsPage.tsx: "The glyph rides .v2-tools__mode's 14px rule (see v2.css)"

That rule carries no dimensions. The gap, tertiary and vertical-align parts are true; the size comes from the call site via G's prop. A reader sent to v2.css to confirm the size will not find it there.

And my recommendation changes. I suggested adding .v2-connector-row__mark svg { width: 14px; height: 14px; } to restore a net. There is no gap to net — G already guarantees a sized svg. So the proportionate close is the comment fix, plus optionally a test pinning size={14} (currently unpinned: dropping it leaves 210/210 green).

Two consequences for the follow-up, TASK-177:

  1. No CSS change is needed, so the __mark-mentioned-exactly-twice invariant does not have to widen.
  2. On rejecting the shared-rule variant — the conclusion is right, the mechanism recorded is not. That rule targets the spans, not their svgs, and a width on a span does not resize a child svg that carries explicit width/height attributes. So 14px there would not shrink the Tools glyphs 16→14; it would put 16px svgs inside 14px spans and misalign them. The stronger reason to reject it is that a span-level rule could never have sized the mark either. (The V2ConnectorTools.tsx:532 / :760 / :824 call sites passing no size is exact — verified.)

Nothing here changes the gate. The hover differential, the 390 behaviours, the gap on both grids, the sheet-integrity counts and the mutation results all stand as measured.

Merged via the queue into main with commit 6b079b1 Sep 27, 2026
18 checks passed
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

One more correction, to my own correction. Gate unchanged: PASS @ 112c9af0.

In the note above I wrote that a 14px width on the shared span rule "would not shrink the Tools glyphs 16→14; it would put 16px svgs inside 14px spans." The numbers are wrong — the Tools glyphs are already 14px. @ux-lead caught it.

V2ConnectorTools.tsx:157 defines its own local G:

const G: React.FC<{ children: React.ReactNode }> = ({ children }) => (
  <svg width="14" height="14" viewBox="0 0 24 24" ...>{children}</svg>
);

Hardcoded 14, no size prop, and the file imports only PlatformGlyph and ActGlyph from ../icons/glyphs — never G, never MarkGlyph. So OutcomeGlyph (:167) and ModeGlyph (:175) render at 14×14 and always have. There are two components named G in two files; I read icons/glyphs.tsx's and assumed it served these.

The mechanism point is unaffected and is in fact firmer: a width on a span does not resize a child svg that carries explicit width/height attributes, and these svgs hardcode them. The claim about the mark also stands, because MarkGlyph is in icons/glyphs.tsx and does use that file's G — default 16, overridden to 14 by this PR's call site.

What this sharpens for the follow-up. The two rows agree at 14px through two independent literals in two files — V2ConnectorTools.tsx:157's hardcoded 14 and size={14} at the MarkGlyph call site — with no shared source and no test. Drop size={14} and the mark goes to 16 while Tools stays at 14, desyncing the two rows the comments claim cannot drift.

So the guard worth writing is not "the mark is 14px" asserted in one place. It is one assertion that the mark and .v2-tools__mode render at the same size, which fails if either literal moves. A single pinned magic number would have survived every misreading in this thread, including both of mine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant